New setting AllowUpdateOverwrite : defines how UPDATE should handle the case when a record was updated by trigger.#8957
Conversation
…e case when a record was updated by trigger.
|
On 3/26/26 12:04, Vlad Khorsun wrote:
After #8538 <#8538>
there was long private discussion with customer (big sponsor of Firebird).
The root of the issue is the way Firebird implements per-row triggers
- it is far from SQL standard in regards of consistency.
There is well-known historical reasons for it and I don't want to
repeat it here.
By my opinion, the best way to fix the issue would be to re-implement
per-row triggers according to SQL standard - to fire before and after
whole statement procesing. But this is not quick nor easy task.
Finally this patch was implemented and customer uses it to prepare own
migration to v5.
Vlad, am I missing something or this patch works only with system
triggers that implement cascade FK?
|
The routine, I modified in the patch, was initially used to workaroud issue with self-referenced FK's |
dyemanov
left a comment
There was a problem hiding this comment.
Honestly, I expected this PR to implement the same logic as seems to exist for FKs:
If field was not changed by user - pick up possible modification by trigger
instead of raising an error unconditionally.
However, AFAIU, we cannot detect this reliably at the VIO layer, because cannot distinguish between new.field explicitly assigned to the old value or copied from org_record. This would need accessing the ModifyNode's assignment list and checking for modified fields there. Although I'm not sure this is going to be as simple.
Did you consider this approach? Because the new mode reminds me the famous "table is mutating" error in Oracle, if raised even for non-conflicting changes.
| # | ||
| # Per-database configurable. | ||
| # | ||
| # Type: integer |
There was a problem hiding this comment.
Why integer and 0/1 instead of boolean and false/true? And rename to something like UpdateOverwriteStrictMode or RestrictUpdateOverwrite?
There was a problem hiding this comment.
Initially I thought about 3rd value - to issue warning, not error. But it was not implemented as I found it unpractical, iirc.
As for the setting name - I can agree with anything reasonable ;) Lets decide if we need such changes.
There was a problem hiding this comment.
Also, I going to add forgoten note that this setting is temporary and could be removed in future versions.
There was a problem hiding this comment.
As for the setting name - I can agree with anything reasonable ;)
I suppose we need more opinions here. @mrotteveel , can you suggest any better naming for a boolean version of the switch?
There was a problem hiding this comment.
How about "DisableTableMutation"?
There was a problem hiding this comment.
Can you explain, please, what is bad with UpdateOverwriteMode ? Don't you like Mode ?
Exactly.
Modesuggests more options than justtrue/false, or maybe two options but explicitly named -- likeenum UpdateOverwriteMode = { Ignore , Restrict }. This is why I initially suggestedStrictModeor alike, because it convertsModeinto a boolean.
AllowUpdateOverwriteTrigger or simple AllowUpdateOverwrite ?
There was a problem hiding this comment.
AllowUpdateOverwriteTriggeror simpleAllowUpdateOverwrite?
I assume you mean these in combination with an enum as Dmitry suggested. Then I'd suggest something like:
TriggerLostUpdateBehaviour = { Allow | Reject }
There was a problem hiding this comment.
AllowUpdateOverwriteTriggeror simpleAllowUpdateOverwrite?I assume you mean these in combination with an enum as Dmitry suggested.
I meant it should be a boolean. I should have state it explicitly, sorry.
Then I'd suggest something like:
TriggerLostUpdateBehaviour = { Allow | Reject }
No, please. "Lost update" is well known term from another area - transaction's isolation and concurrency.
And there is no "lost update" in classical meaning here.
There was a problem hiding this comment.
AllowUpdateOverwriteTriggeror simpleAllowUpdateOverwrite?
Either AllowUpdateOverwrite[Trigger] (with default = true) or RejectUpdateOverwrite[Trigger] (with default = false) are fine with me.
There was a problem hiding this comment.
Ok, I'll change it by AllowUpdateOverwriteTrigger with default = true.
| const bool flag_cur = EVL_field(relation, cur_rpb->rpb_record, fld, &dsc_cur); | ||
|
|
||
| // Check if current record differs from old record | ||
| if ((flag_cur != flag_old) || |
There was a problem hiding this comment.
If the field didn't exist in the old format but exists in the new format and gets some value updated by a trigger (e.g. because the new field is NOT NULL), an error will be raised. Is it intended?
There was a problem hiding this comment.
If any field of current record was changed by trigger, that fired for another record - yes, this is error.
Despite of formats.
I consider this as wrong way because it let user triggers to implicitly change original UPDATE semantic. Case with self-referenced FK's is more predictable and consistent (unless same fields not changed by user trigger).
No. By the reasons as explained above.
The more I think on this issue, the more I agree with Oracle - it should not be allowed. And, actually, I've implemented such check when works on the issue past year. But - not a surprise - it was too restrictive and customer does not accepted it. Finally, the setting UpdateOverwriteMode was introduced and agreed as compromise solution. Ideally, we should reimplement the way per-row triggers works to provide consistensy and comply with SQL standard. |
|
Well, provided that it's disabled by default, I can accept this patch. Did you try running the QA pass with |
Yes, but I don't remember the results, sorry. Will try once more time. |
Found no differences. Same number and list of failed and passed tests in two runs. |
I'm sorry, missed one failed test: tests\bugs\core_3362_complex_test.py It shows one more (forgotten) case when update could silently overwrite prior update - when second update is positioned on explicit (stable) cursor: Here: update (2) silently overwrites changes by update (1) and error is raised when I consider this as correct behavior that should be properly documented. Test case |
|
I don't think that raising an error on two separate updates that operate on the same record is a correct behavior. |
You missed the point - updates are not separate. Specially for you: Most users expects to see (1, 'a', 'b') here but actuall result is (1, '', 'b') |
|
Yes, this point I missed. I would also expect |
Not a bug but implementation detail. Maybe not so obvious. Also, nobody forces you to set I'm not happy to introduce such settings, but see no better alternative. At least for the stable branch. |
How about my suggestion above? Result is the same - the error is thrown, but at least it is thrown consistently for all cases "two updates in one routine", "trigger then main request" and "main request then trigger". All these cases can be fixed by changing of user code. |
As far as I understand your suggestion (to reuse MERGE logic): Also, the patch I offer is in use by the customer for a few months already and got intensive testing. |
Don't pay so much attention to the comparison. From "MERGE logic" in the suggestion is only throwing of an error on double update of the same record inside of the same statement.
Perhaps you read it wrong. The proposed loop through savepoints can detect double update on any (desirable) level. No matter how deep is current update, the update on any higher level is reliable detected. Positioned update is not different to any other update in regard of marking a record as "touched" in |
BTW, you don't need to throw whole your work away, just change record modification detection from fields' comparison to looking at backlog. |
|
How your "loop through savepoints" will handle this case ? |
By throwing an error that "allows users to detect such code and fix it to not produce such behavior". And only in the case if they purposefully "set |
Wasted time, again. Don't write to my topics anymore. |
|
Do you care to explain how this example is different from core_3362_complex_test? |
Find the stable cursor and positioned update, if you know what these words means. |
…Mode New setting AllowUpdateOverwrite : defines how UPDATE should handle the case when a record was updated by trigger.
After #8538 there was long private discussion with customer (big sponsor of Firebird).
The root of the issue is the way Firebird implements per-row triggers - it is far from SQL standard in regards of consistency.
There is well-known historical reasons for it and I don't want to repeat it here.
By my opinion, the best way to fix the issue would be to re-implement per-row triggers according to SQL standard - to fire before and after whole statement processing. But this is not quick nor easy task.
Finally this patch was implemented and customer uses it to prepare own migration to v5.
Now it is time to decide if it could be accepted into codebase.